Skip to content

feat(plugin-auth): the SSO Providers form sets, shows and clears the groups claim - #31755

Merged
objectstack-fleet[bot] merged 10 commits into
mainfrom
claude/project-thread-d7w1a6
Nov 9, 2026
Merged

objectstack-fleet[bot] merged 10 commits into
mainfrom
claude/project-thread-d7w1a6

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread

Fixes #31666

Clause-②: yes (widening)
Metadata-protocol: none

维护者速读

  • 改了什么:Setup → SSO Providers 表单现在能设置、查看和清除 SSO 组声明(groups claim),写入的位置与 /sso/register 完全相同:mapping.extraFields.groups。
  • 为什么改:feat(plugin-auth): an SSO sign-in grants the permission set its IdP group is mapped to #31657 之后登录会按 IdP 组授予权限集,但组声明只能经 API 配置,管理员在界面上无法完成这一步。
  • 风险与回滚:只新增两个仅限平台管理员的动作和两个注册参数;另修了一个现存缺陷(带 oidcConfig 的 update-provider 对密钥存于 sys_secret 的 OIDC 提供方一律返回 400)。回滚即 revert 本 PR。
  • 先看:packages/plugins/plugin-auth/src/register-sso-provider.ts 末尾的两个桥,以及 objectql-adapter.ts 里 update 回显的密钥注入。
  • 两分钟验证:pnpm --filter @objectstack/plugin-auth exec vitest run src/sso-oidc-refusal-cause.test.ts src/register-sso-provider.test.ts src/admin-sso-bridge-gate.test.ts。
  • 需要你做的:无;两个新路由名由席位裁定(见下)。

What changes

The Setup → SSO Providers form is metadata (sys_sso_provider actions with flat params that post to plugin-auth /admin/sso/* bridges), not console code, so the whole change sits in plugin-auth and platform-objects. Nothing under ui/ or packages/spec/** changes.

  • Register SSO Provider gains Map: Groups claim and Register SAML Provider gains Map: Groups attribute. A non-blank value lands on mapping.extraFields.groups, exactly where POST /api/v1/auth/sso/register stores it. Blank stores no extraFields, as today.
  • Show Groups Claim posts to the new POST /api/v1/auth/admin/sso/get-groups-claim, which reads the stored row (system read, oidc_config / saml_config recovered) and answers only { providerId, groupsClaim }.
  • Edit Groups Claim posts to the new POST /api/v1/auth/admin/sso/set-groups-claim. It merges the claim into the stored mapping (every other member kept verbatim), writes through the vendor's own /sso/update-provider, and reads the claim back. A blank claim deletes extraFields.groups (and an emptied extraFields), so the provider signs users in exactly like one saved without it.
  • Both routes run gateAdmin first, the platform-admin gate of the other /admin/sso bridges. A refused vendor update answers its status with the standard code for it and a fixed sentence; the vendor's body is never relayed, so no other config member (client secret, certificate, endpoints, other mapping members) can reach an answer.
  • Fix: @better-auth/sso reads the provider for /sso/update-provider through lockSSOProviderRow, which is an adapter update. That echo did not run the read half (injectClientSecretOnRead), so any update-provider carrying an oidcConfig for a provider whose secret is held in sys_secret was refused 400 "clientSecret is required". The adapter's update now carries the secret and the IdP-initiated column into its echo like every other provider read.
  • Two route-ledger rows, two platform-objects affordance rows, docs page content/docs/permissions/sso.mdx updated, translations for ten locales.
  • The feat(plugin-auth): an SSO sign-in grants the permission set its IdP group is mapped to #31657 grant logic (sso-group-projection.ts) is untouched.

New public route names (/admin/sso/get-groups-claim, /admin/sso/set-groups-claim) and action names (show_sso_groups_claim, edit_sso_groups_claim) follow the /admin/sso/* and *_sso_* house spelling; they are the seat's call. No new error code: 400 VALIDATION_ERROR / INVALID_REQUEST, 404 RESOURCE_NOT_FOUND, 401/403 from the existing gate.

Why

Since #31657 an SSO sign-in grants the permission sets mapped to the provider's IdP groups, read from mapping.extraFields.groups. Only the vendor register API could set that member (documented by #31664); an administrator working in Setup had no way to configure it.

Verification

Clause-② measured: on base POST /api/v1/auth/admin/sso/set-groups-claim and get-groups-claim are not mounted and the register forms carry no groups param; on head both routes answer a platform admin 200 and the register forms accept the new param. A widening only: no request accepted on main is now refused.

Tests (all import src):

  • sso-oidc-refusal-cause.test.ts, new describe: a form save with a claim stores the register door's mapping and the next sign-in grants the mapped set (the feat(plugin-auth): an SSO sign-in grants the permission set its IdP group is mapped to #31657 pin shape). CONTROL: a blank claim writes nothing and grants nothing. CONTROL: Show on a register-door row answers groups, then a clear removes it. Round trip on a rich mapping (register-door row and a row this action changed): every other member kept verbatim.
  • register-sso-provider.test.ts: both register forms store the claim where the register door does (compared to a direct /sso/register row), CONTROL without it; the bridges' merge, clear, 404 and 400; a no-leak pin (client secret, certificate, client id, IdP URL, other mapping members never appear in any answer, including a vendor refusal carrying them; data keys are exactly providerId and groupsClaim).
  • admin-sso-bridge-gate.test.ts: for both routes, anonymous 401 and member / org-admin / position-named-admin 403 with neither the store read nor the vendor called; platform admin 200.
  • sso-client-secret-at-rest.test.ts ③b: an update-provider carrying only oidcConfig.mapping is accepted and the secret still reads back.

Ablations (each restored, git diff HEAD empty afterwards):

  • Gate removed from set-groups-claim → 4 gate tests red (expected 200 to be 401 / 403).
  • Vendor refusal body relayed → the no-leak test red.
  • Mapping added to the show answer → 2 tests red (data had 3 keys, expected 2).
  • Echo injection removed from the adapter update → ③b and the round trip red (clientSecret is required …, 400).

Runs: plugin-auth full suite 4431 passed, 10 skipped, 1 red (the route-ledger pin, which hand-lists the mounts); that file re-run green (10/10) after the pin named the two routes; platform-objects 120 files / 1933 tests green; typecheck green for both packages; lint (eslint --no-inline-config) clean on both packages; check:i18n, check:i18n-coverage and check:auth-mount-ledger OK; every dispatch-gates --commands command run and reconciled with --ran. Dogfood, run locally under the verify lock after the sweep fix: the whole @objectstack/dogfood package 440 files passed, 3 skipped; 3653 tests passed, 41 skipped; 0 failed. The five admin suites together 5/5 files, 34/34 tests. Control: without the new entries the non-admin sweep is red at :618 with the queue's signature.

Forced test edits (existing tests changed because the surface grew):

  • sso-oidc-refusal-cause.test.ts: bootGroupEngine now registers the auth object extensions and sys_secret and sets a fake crypto provider, so the harness holds a client secret the way production does.
  • platform-objects.test.ts: two rows in the action table (show_sso_groups_claim, edit_sso_groups_claim).
  • platform-admin-affordance-standing.test.ts: the same two actions on the floor list.
  • auth-route-ledger.conformance.test.ts: the hand-written objectstack-mount pin names the two new routes.
  • packages/qa/dogfood/test/admin-platform-admin-standing.dogfood.test.ts: the two routes join ADMITTED (ADR-0068 gate first) with probe bodies; the admin reaches the handler and gets 404 for the unknown probe provider, nothing moves.
  • packages/qa/dogfood/test/admin-route-nonadmin-refusal.dogfood.test.ts: the two routes join the objectstack-gate bucket with the same probe bodies (anonymous 401, member 403; admin measured 404 RESOURCE_NOT_FOUND).

Both dogfood sweeps derive the served /admin/ route set and refuse an unclassified mount; PR CI does not run the dogfood shards, so the merge queue caught them (run 42562448270, Dogfood 3/3; then Dogfood 1/3). Census of packages/qa/dogfood/test: only these two files derive the /admin/ set. admin-credential-lifecycle, admin-identity-audit-trail and admin-ledger-decision-metadata name fixed routes and need no registration.

Out of scope, noted: ADR-0011's "Not assessed" list could name the two new actions (governed surface, left alone); the cloud auth proxy must mount the two new routes to serve them there.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (Not touched.)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English. (The ## 维护者速读 section is Chinese by the dispatch order.)
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

… client secret; pins for the form groups claim

Claude-Session: https://claude.ai/code/session_01ADHkmQznRNHwfSHVJJUCj2
Co-authored-by: Claude <noreply@anthropic.com>
… in ten locales; changeset names the update-provider fix

Claude-Session: https://claude.ai/code/session_01ADHkmQznRNHwfSHVJJUCj2
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 9, 2026
@github-actions

github-actions Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/platform-objects, @objectstack/plugin-auth, touching 53 documentable anchor(s).

44 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 6be71abc34cdd7adcc6057d121479ba5a11724e3.

⛔ 11 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 65 of 231 client-bound route-ledger rows — the other 166 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 166: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 59 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 107 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 24 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 6be71abc34cdd7adcc6057d121479ba5a11724e3 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 96c5dbba8cc7814cb4020ab9e86d514cc361189b — the merge of head 26b9e907ba78ffdd6ddfb7fe1bf0cdd690f4c2e6 into base 6be71abc34cdd7adcc6057d121479ba5a11724e3, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 96c5dbba8cc7814cb4020ab9e86d514cc361189b && git checkout 96c5dbba8cc7814cb4020ab9e86d514cc361189b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6be71abc34cdd7adcc6057d121479ba5a11724e3 26b9e907ba78ffdd6ddfb7fe1bf0cdd690f4c2e6 && git checkout -B drift-repro 6be71abc34cdd7adcc6057d121479ba5a11724e3 && git merge --no-ff 26b9e907ba78ffdd6ddfb7fe1bf0cdd690f4c2e6

node scripts/docs-audit/affected-docs.mjs --json 6be71abc34cdd7adcc6057d121479ba5a11724e3

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 6be71abc34cdd7adcc6057d121479ba5a11724e3 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-steve · session_01ADHkmPF1skfu2kx89BEXZi

席位 ACCEPT · head 80a1f4cced8544774eaebe3c140f8fe1f001e63a

面:declaration(Clause-②: yes (widening),无 schema、无受治理路径),按 TIER_DEFAULT 由席位审。

  • 表单 → 存储:两个注册表单加了 mapGroups,写到 mapping.extraFields.groups,与 /sso/register 的写法完全相同(mappingWithGroupsClaim)。SAML 只在给了组声明时才写 mapping,不给就不建 mapping,所以没有新增存储位置,存储形状也不变。
  • Show / Edit Groups Claim:两个新桥 /admin/sso/get-groups-claim、/admin/sso/set-groups-claim 都先过 gateAdmin(ADR-0068 D4 平台管理员门,和另外四个 /admin/sso/* 桥同一个门)。读是系统身份的 findOne 加 recoverInternalFieldsForSystemRead,应答只有 {providerId, groupsClaim}。写走 update-provider 合并写,其他 mapping 成员逐字保留,空值即清除。厂商的拒绝体不转发。
  • 顺带修的现存缺陷(objectql-adapter 读回路径补上 injectClientSecretOnRead):没有它,任何带 oidcConfig 的 update-provider,遇到 sys_secret 托管密钥的 OIDC 提供方都会回 400,本卡的编辑路径会走不通。修复必要、范围小,③b 钉住并做了消融。接受为偏差。
  • 约束 1–3 都有测试:不泄露的负向钉(密钥、证书、client id、IdP URL、其他 mapping 成员都不进任何应答)、非管理员 401/403、两种行的往返。4 次消融全红后已还原。feat(plugin-auth): an SSO sign-in grants the permission set its IdP group is mapped to #31657 的授予逻辑没动。
  • 新的路由名和动作名是席位的事,按现名接受。
  • 跨仓:cloud 的 AuthProxyPlugin 要挂上这两条新路由。落地后由席位通知 cloud 席。
  • 门禁对账 108/108,0 UNRUN。check-governed-merges → NOT governed,1330 行。

CI 全绿后落地:relay pr_ready + automerge_enable。

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 9, 2026 04:32
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 9, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Nov 9, 2026
@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 42562448270 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Dogfood Regression Gate (3/3) — 失败步骤: Boot example apps and exercise real user flows

    @objectstack/dogfood:test:  FAIL   isolated  test/admin-platform-admin-standing.dogfood.test.ts > #9482: what an ObjectStack platform admin gets from every /admin/ route > no /admin/ route refuses the
      ↳ 失败原因: @objectstack/dogfood:test: AssertionError: the stack serves /admin/ route(s) this file neither admits, refuses-by-design, nor classifies as answering for a non-authorization reason. A new admin route 
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 16 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

claude and others added 2 commits November 9, 2026 05:30
…unts

The platform-admin standing sweep admits get-groups-claim and
set-groups-claim (ADR-0068 gate first; the probe provider is unknown, so the
admin lands on 404 and nothing moves). The non-admin refusal sweep records
them in the objectstack-gate bucket with the same probe bodies.

Claude-Session: https://claude.ai/code/session_01ADHkmQznRNHwfSHVJJUCj2
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-steve · session_01ADHkmPF1skfu2kx89BEXZi

席位 ACCEPT (delta) · head 78d7c1c1bd5cae3d88aa07e7a3f85c7218789c01

This is a delta on ACCEPT at 80a1f4cc. It covers the queue ejection fix only, and the main merge that follows it is not part of the review.

  • The queue ejected this PR twice with the same cause: the two new /admin/sso/* routes were not registered in the two dogfood suites that derive every /admin/ route (tests(identity): pin the unpinned admin-lifecycle clauses of identity-auth.admin-lifecycle-operations — coverage gaps from QA run #9401 #9482). PR CI runs only the dogfood aggregate, so it never ran them.
  • The fix registers both routes:
    • in admin-platform-admin-standing ADMITTED, with probe bodies. The admin side answers 404 RESOURCE_NOT_FOUND, because the probe provider is absent and nothing is written.
    • in admin-route-nonadmin-refusal EXPECTED, in the objectstack-gate bucket.
  • Control: without the entries, :618 is red with the queue signature.
  • Census: only these two dogfood files derive the /admin/ set. The three sibling files name fixed routes and need no registration.
  • The whole dogfood package passed locally: 440 files and 3653 tests, 0 failed.
  • The body is patched (forced test edits, runs). Auto-merge was disabled after the second ejection. It is re-queued only once CI on the merged head is all green.

Merged via the queue into main with commit 96b9350 Nov 9, 2026
56 of 57 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-d7w1a6 branch November 9, 2026 06:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

setup: the SSO Providers form cannot set the groups claim mapping, so IdP group mapping needs the vendor register door

2 participants